package com.frank.oj.manager;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.frank.model.entity.user.*;
import com.frank.oj.common.exception.StatusAccessDeniedException;
import com.frank.oj.common.exception.StatusFailException;
import com.frank.oj.config.NacosSwitchConfig;
import com.frank.oj.config.WebConfig;
import com.frank.oj.constant.Constants;
import com.frank.oj.model.bo.EmailRuleBO;
import com.frank.oj.model.dto.ApplyResetPasswordDTO;
import com.frank.oj.model.dto.LoginDTO;
import com.frank.oj.model.dto.RegisterDTO;
import com.frank.oj.model.dto.ResetPasswordDTO;
import com.frank.oj.model.vo.RegisterCodeVO;
import com.frank.oj.model.vo.UserInfoVO;
import com.frank.oj.model.vo.UserRolesVO;
import com.frank.oj.service.admin.system.SystemLogService;
import com.frank.oj.service.entity.user.UserRecordEntityService;
import com.frank.oj.service.entity.user.UserInfoEntityService;
import com.frank.oj.service.entity.user.UserRoleEntityService;
import com.frank.oj.service.entity.user.SessionEntryService;
import com.frank.oj.shiro.AccountProfile;
import com.frank.oj.utils.IpUtils;
import com.frank.oj.utils.JwtUtils;
import com.frank.oj.utils.RedisUtils;
import com.jthinking.common.util.ip.IPInfo;
import com.jthinking.common.util.ip.IPInfoUtils;
import org.apache.shiro.SecurityUtils;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * @author frank
 * @Date 2024/1/25
 */
@Component
public class PassportManager {
    @Resource
    private JwtUtils jwtUtils;
    @Resource
    private RedisUtils redisUtils;
    @Resource
    private UserRoleEntityService userRoleEntityService;
    @Resource
    private SessionEntryService sessionEntryService;
    @Resource
    private NacosSwitchConfig nacosSwitchConfig;
    @Resource
    private UserInfoEntityService userInfoEntityService;
    @Resource
    private UserRecordEntityService userRecordEntityService;
    @Resource
    private SystemLogService systemLogService;
    @Resource
    private NoticeManager noticeManager;
    @Resource
    private EmailManager emailManager;
    @Resource
    private EmailRuleBO emailRuleBO;

    public void logout() {
        AccountProfile userRole = (AccountProfile) SecurityUtils.getSubject().getPrincipal();
        jwtUtils.cleanToken(userRole.getUid());
        SecurityUtils.getSubject().logout();
    }

    public UserInfoVO login(LoginDTO loginDTO, HttpServletRequest request, HttpServletResponse response) throws StatusFailException {
        //    去收尾空格
        loginDTO.setUsername(loginDTO.getUsername().trim());
        loginDTO.setPassword(loginDTO.getPassword().trim());
        loginDTO.setCaptcha(loginDTO.getCaptcha().trim());
        loginDTO.setCaptchaKey(loginDTO.getCaptchaKey().trim());

        if (StringUtils.isEmpty(loginDTO.getUsername()) || StringUtils.isEmpty(loginDTO.getPassword())
                || StringUtils.isEmpty(loginDTO.getCaptchaKey()) || StringUtils.isEmpty(loginDTO.getCaptcha())) {
            throw new StatusFailException("用户名、密码及验证码不能为空");
        }
        //限制用户名和密码的长度
        if (loginDTO.getPassword().length() < 6 || loginDTO.getPassword().length() > 20) {
            throw new StatusFailException("密码长度在6-20位之间");
        }
        if (loginDTO.getUsername().length() > 20) {
            throw new StatusFailException("用户名长度过长（不超过20位）");
        }

        String code = (String) redisUtils.get(loginDTO.getCaptchaKey());
        if (!Objects.equals(code, loginDTO.getCaptcha().toLowerCase())) {
            throw new StatusFailException("验证码错误");
        }
        //获取ip地址
        String userIpAddr = IpUtils.getUserIpAddr(request);
        //生成登录限制的redis-key
        String key = Constants.Account.TRY_LOGIN_NUM.getCode() + loginDTO.getUsername() + "_" + userIpAddr;

        Integer tryLoginCount = (Integer) redisUtils.get(key);

        if (tryLoginCount != null && tryLoginCount >= 20) {
            throw new StatusFailException("对不起！登录失败次数过多！账号可能存在风险，请半小时后再试！");
        }

        //获取用户信息
        UserRolesVO userRole = userRoleEntityService.getUserRoles(null, loginDTO.getUsername());
        if (userRole == null) {
            throw new StatusFailException("用户名或密码错误，请注意大小写！");
        }
        if (!userRole.getPassword().equals(SecureUtil.md5(loginDTO.getPassword()))) {
            if (tryLoginCount == null) {
                redisUtils.set(key, 1, 60 * 30);
            } else {
                redisUtils.set(key, tryLoginCount + 1, 60 * 30);
            }
            throw new StatusFailException("用户名或密码错误，请注意大小写！");
        }
        //账号状态：0可用，1不可用
        if (userRole.getStatus() != 0) {
            throw new StatusFailException("该账号存在违规，已被封禁！请联系管理员进行处理！");
        }
        //生成jwt-token
        String jwt = jwtUtils.generateToken(userRole.getUid());
        response.setHeader("Authorization", jwt);
        response.setHeader("Access-Control-Expose-Headers", "Authorization");

        sessionEntryService.save(new Session()
                .setUid(userRole.getUid())
                .setIp(IpUtils.getUserIpAddr(request))
                .setUserAgent(request.getHeader("User-Agent")));

        String address = null;
        if (Objects.equals(IpUtils.getUserIpAddr(request), "127.0.0.1")) {
            address = "localhost";
        } else {
            IPInfo ipInfo = IPInfoUtils.getIpInfo(IpUtils.getUserIpAddr(request));
            address = ipInfo.getCountry() + ":" + ipInfo.getProvince() + ipInfo.getAddress() + "|" + ipInfo.getIsp();
        }
        String header = request.getHeader("User-Agent");
        String agent = header.substring(header.indexOf("(") + 1, header.indexOf(")")).split(";")[1];

        systemLogService.save(new SystemLog()
                .setUsername(userRole.getUsername())
                .setIp(IpUtils.getUserIpAddr(request))
                .setAddress(address)
                .setAgent(agent));

        //删除key，解除重试次数的限制
        if (tryLoginCount != null) {
            redisUtils.del(key);
        }

        sessionEntryService.checkRemoteLogin(userRole.getUid());

        UserInfoVO userInfoVO = new UserInfoVO();
        BeanUtil.copyProperties(userRole, userInfoVO, "roles");
        userInfoVO.setRoleList(userRole.getRoles()
                .stream()
                .map(Role::getRole)
                .collect(Collectors.toList()));

        return userInfoVO;
    }

    @Transactional(rollbackFor = Exception.class)
    public void register(RegisterDTO registerDTO) throws StatusFailException, StatusAccessDeniedException {
        WebConfig webConfig = nacosSwitchConfig.getWebConfig();
        //判断是否开启注册功能
        if (!webConfig.getRegister()) {
            throw new StatusAccessDeniedException("暂未开启注册功能，请联系管理员！");
        }

        String codeKey = Constants.Email.REGISTER_KEY_PREFIX.getValue() + registerDTO.getEmail();
        if (!redisUtils.hasKey(codeKey)) {
            throw new StatusFailException("验证码已过期，请重新获取验证码！");
        }
        if (!redisUtils.get(codeKey).equals(registerDTO.getCode())) {
            throw new StatusFailException("验证码错误，请重新获取验证码！");
        }
        if (StringUtils.isEmpty(registerDTO.getPassword())) {
            throw new StatusFailException("密码不能为空");
        }
        if (registerDTO.getPassword().length() > 20 || registerDTO.getPassword().length() < 6) {
            throw new StatusFailException("密码长度应为6~20位!");
        }
        if (StringUtils.isEmpty(registerDTO.getUsername())) {
            throw new StatusFailException("用户名不能为空");
        }
        if (registerDTO.getUsername().length() > 20) {
            throw new StatusFailException("用户名长度过长（不超过20位）");
        }

        String uuid = IdUtil.simpleUUID();

        registerDTO.setUuid(uuid);
        registerDTO.setPassword(SecureUtil.md5(registerDTO.getPassword().trim()));
        registerDTO.setUsername(registerDTO.getUsername().trim());
        registerDTO.setEmail(registerDTO.getEmail().trim());

        boolean addUser = userInfoEntityService.addUser(registerDTO);

        String random = RandomUtil.randomString(5);
        String nikeName = "HC-" + random;

        LambdaUpdateWrapper<UserInfo> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.set(UserInfo::getNickname, nikeName).eq(UserInfo::getUuid, uuid);
        boolean updated = userInfoEntityService.update(updateWrapper);

        boolean addUserRole = userRoleEntityService.save(new UserRole().setRoleId(1002L).setUid(uuid));
        boolean addUserRecord = userRecordEntityService.save(new UserRecord().setUid(uuid));

        if (addUser && addUserRole && addUserRecord && updated) {
            // todo 有异议
            redisUtils.del(registerDTO.getEmail());
            noticeManager.syncNoticeToNewRegisterUser(uuid);
        } else {
            throw new StatusFailException("注册失败，请稍后重试！");
        }
    }


    public RegisterCodeVO getRegisterCode(String email) throws StatusAccessDeniedException, StatusFailException {
        WebConfig webConfig = nacosSwitchConfig.getWebConfig();
        if (!webConfig.getRegister()) {
            throw new StatusAccessDeniedException("当前未启用个人注册功能，如需注册请联系管理员！");
        }
        if (!emailManager.isOk()) {
            throw new StatusAccessDeniedException("当前邮箱服务不可用，请稍后重试！");
        }
        email = email.trim();
        boolean isEmail = Validator.isEmail(email);
        if (!isEmail) {
            throw new StatusFailException("邮箱格式错误！");
        }
        boolean isBlackEmail = emailRuleBO.getBlackList().stream().anyMatch(email::endsWith);
        if (isBlackEmail) {
            throw new StatusFailException("邮箱后缀被禁止注册，请切换另一家邮箱重试！");
        }
        String lockKey = Constants.Email.REGISTER_EMAIL_LOCK + email;
        if (redisUtils.hasKey(lockKey)) {
            throw new StatusFailException("对不起，您的操作频率过快，请在" + redisUtils.getExpire(lockKey) + "秒后再次发送注册邮件！");
        }

        LambdaQueryWrapper<UserInfo> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(UserInfo::getEmail, email);
        UserInfo userInfo = userInfoEntityService.getOne(queryWrapper);
        if (userInfo != null) {
            throw new StatusFailException("该邮箱已被注册，请更换邮箱后重试！");
        }
        String code = RandomUtil.randomNumbers(6);
        redisUtils.set(Constants.Email.REGISTER_KEY_PREFIX.getValue() + email, code, 5 * 60);
        emailManager.sendRegisterCode(email, code);
        redisUtils.set(lockKey, 0, 60);

        RegisterCodeVO registerCodeVO = new RegisterCodeVO();
        registerCodeVO.setEmail(email);
        registerCodeVO.setExpire(5 * 60);

        return registerCodeVO;
    }

    public void applyResetPassword(ApplyResetPasswordDTO applyResetPasswordDto) throws StatusFailException {
        String captcha = applyResetPasswordDto.getCaptcha();
        String captchaKey = applyResetPasswordDto.getCaptchaKey();
        String email = applyResetPasswordDto.getEmail();

        if (StrUtil.isBlank(captcha) || StrUtil.isBlank(email) || StrUtil.isBlank(captchaKey)) {
            throw new StatusFailException("邮箱或验证码不能为空");
        }
        if (!emailManager.isOk()) {
            throw new StatusFailException("当前邮箱服务不可用，请稍后重试！");
        }

        String lockKey = Constants.Email.RESET_EMAIL_LOCK + email;
        if (redisUtils.hasKey(lockKey)) {
            throw new StatusFailException("对不起，您的操作频率过快，请在" + redisUtils.getExpire(lockKey) + "秒后再次发送重置邮件！");
        }

        String redisCode = (String) redisUtils.get(captchaKey);
        if (!Objects.equals(redisCode, captcha.trim().toLowerCase())) {
            throw new StatusFailException("验证码错误！");
        }

        LambdaQueryWrapper<UserInfo> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(UserInfo::getEmail, email.trim());
        UserInfo userInfo = userInfoEntityService.getOne(queryWrapper, false);
        if (userInfo == null) {
            throw new StatusFailException("该邮箱尚未注册，请先注册！");
        }

        String code = IdUtil.simpleUUID().substring(0, 21);
        redisUtils.set(Constants.Email.RESET_PASSWORD_KEY_PREFIX.getValue() + userInfo.getUsername(), code, 5 * 60);//默认链接有效5分钟

        emailManager.sendResetPassword(userInfo.getUsername(), code, email.trim());
        redisUtils.set(lockKey, 0, 90);
    }

    public void resetPassword(ResetPasswordDTO resetPasswordDto) throws StatusFailException {
        String username = resetPasswordDto.getUsername();
        String password = resetPasswordDto.getPassword();
        String code = resetPasswordDto.getCode();

        if (StringUtils.isEmpty(password) || StringUtils.isEmpty(username) || StringUtils.isEmpty(code)) {
            throw new StatusFailException("用户名、密码或验证码不能为空！");
        }
        if (password.length() < 6 || password.length() > 20) {
            throw new StatusFailException("新密码长度应该为6~20位！");
        }
        String codeKey = Constants.Email.RESET_PASSWORD_KEY_PREFIX.getValue() + username;
        if (!redisUtils.hasKey(codeKey)) {
            throw new StatusFailException("验证码已过期，请重新发送邮件！");
        }
        if (!redisUtils.get(codeKey).equals(code)) {
            throw new StatusFailException("验证码错误！");
        }

        LambdaUpdateWrapper<UserInfo> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.eq(UserInfo::getUsername, username).set(UserInfo::getPassword, SecureUtil.md5(password));
        boolean updated = userInfoEntityService.update(updateWrapper);
        if (!updated) {
            throw new StatusFailException("重置密码失败，请稍后重试！");
        }

        redisUtils.del(codeKey);
    }
}
